﻿
namespace IssueVision.Data.Web
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.Data;
    using System.Linq;
    using System.ServiceModel.DomainServices.EntityFramework;
    using System.ServiceModel.DomainServices.Hosting;
    using System.ServiceModel.DomainServices.Server;


    // Implements application logic using the IssueVisionEntities context.
    // TODO: Add your application logic to these methods or in additional methods.
    // TODO: Wire up authentication (Windows/ASP.NET Forms) and uncomment the following to disable anonymous access
    // Also consider adding roles to restrict access as appropriate.
    [EnableClientAccess]
    [RequiresAuthentication]
    public class IssueVisionService : LinqToEntitiesDomainService<IssueVisionEntities>
    {

        public IQueryable<Attribute> GetAttributes()
        {
            return ObjectContext.Attributes;
        }

        public void InsertAttribute(Attribute attribute)
        {
            if (attribute.ID == Guid.Empty)
                attribute.ID = Guid.NewGuid();

            if ((attribute.EntityState != EntityState.Detached))
            {
                ObjectContext.ObjectStateManager.ChangeObjectState(attribute, EntityState.Added);
            }
            else
            {
                ObjectContext.Attributes.AddObject(attribute);
            }
        }

        public void UpdateAttribute(Attribute currentAttribute)
        {
            ObjectContext.Attributes.AttachAsModified(currentAttribute, ChangeSet.GetOriginal(currentAttribute));
        }

        public void DeleteAttribute(Attribute attribute)
        {
            if ((attribute.EntityState == EntityState.Detached))
            {
                ObjectContext.Attributes.Attach(attribute);
            }
            ObjectContext.Attributes.DeleteObject(attribute);
        }

        public IQueryable<File> GetFiles()
        {
            return ObjectContext.Files;
        }

        public void InsertFile(File file)
        {
            if (file.FileID == Guid.Empty)
                file.FileID = Guid.NewGuid();

            if ((file.EntityState != EntityState.Detached))
            {
                ObjectContext.ObjectStateManager.ChangeObjectState(file, EntityState.Added);
            }
            else
            {
                ObjectContext.Files.AddObject(file);
            }
        }

        public void UpdateFile(File currentFile)
        {
            ObjectContext.Files.AttachAsModified(currentFile, ChangeSet.GetOriginal(currentFile));
        }

        public void DeleteFile(File file)
        {
            if ((file.EntityState == EntityState.Detached))
            {
                ObjectContext.Files.Attach(file);
            }
            ObjectContext.Files.DeleteObject(file);
        }

        /// <summary>
        /// Get all issues orderby by StatusID, and Priority
        /// </summary>
        /// <returns></returns>
        public IQueryable<Issue> GetIssues()
        {
            return ObjectContext.Issues
                .Include("Files").Include("Attributes")
                .OrderBy(g => g.StatusID).ThenBy(g => g.Priority);
        }

        /// <summary>
        /// Get all un-resolved issues
        /// </summary>
        /// <returns></returns>
        public IQueryable<Issue> GetAllUnResolvedIssues()
        {
            return ObjectContext.Issues
                .Where(n => (n.ResolutionID == null || n.ResolutionID == 0));
        }

        /// <summary>
        /// Get all issues for the current user orderby by StatusID, and Priority
        /// </summary>
        /// <returns></returns>
        public IQueryable<Issue> GetMyIssues()
        {
            return ObjectContext.Issues
                .Include("Files").Include("Attributes")
                .Where(g => g.AssignedToID.Equals(ServiceContext.User.Identity.Name))
                .OrderBy(g => g.StatusID).ThenBy(g => g.Priority);
        }

        /// <summary>
        /// When a new issue is created, we should check the following:
        /// 
        /// 1) OpenedDate, OpenedByID, LastChange, changedByID, and IssueID
        ///    should be set with their initial values.
        /// 2) If status is Open, the AssignToID should be null.
        /// 3) ResolutionDate and ResolvedByID should be set based on ResolutionID.
        /// </summary>
        /// <param name="issue"></param>
        public void InsertIssue(Issue issue)
        {
            issue.OpenedDate = DateTime.Now;
            issue.OpenedByID = ServiceContext.User.Identity.Name;
            issue.LastChange = DateTime.Now;
            issue.ChangedByID = ServiceContext.User.Identity.Name;
            // create a new Issue ID
            issue.IssueID = ObjectContext.Issues.Count() > 0 ? (from iss in ObjectContext.Issues select iss.IssueID).Max() + 1 : 1;
            // if status is Open, AssignedToID should be null
            if (issue.StatusID == IssueVisionServiceConstant.OpenStatusID)
            {
                issue.AssignedToID = null;
            }
            // set ResolutionDate and ResolvedByID based on ResolutionID
            if (issue.ResolutionID == null || issue.ResolutionID == 0)
            {
                issue.ResolutionDate = null;
                issue.ResolvedByID = null;
            }
            else
            {
                if (issue.ResolutionDate == null)
                    issue.ResolutionDate = DateTime.Now;
                if (issue.ResolvedByID == null)
                    issue.ResolvedByID = ServiceContext.User.Identity.Name;
            }

            if ((issue.EntityState != EntityState.Detached))
            {
                ObjectContext.ObjectStateManager.ChangeObjectState(issue, EntityState.Added);
            }
            else
            {
                ObjectContext.Issues.AddObject(issue);
            }
        }

        /// <summary>
        /// When a new issue is updated, we should check the following:
        /// 
        /// 1) LastChange and changedByID should be upated.
        /// 2) If status is Open, the AssignToID should be null.
        /// 3) ResolutionDate and ResolvedByID should be set based on ResolutionID.
        /// </summary>
        /// <param name="issue"></param>
        public void UpdateIssue(Issue issue)
        {
            // Business logic:
            // Admin user can read/update any issue, and
            // normal user can only read/update issues assigned to them
            // or issues created by them and have not assigned to anyone.
            if (!IssueIsReadOnly(ChangeSet.GetOriginal(issue)))
            {
                issue.LastChange = DateTime.Now;
                issue.ChangedByID = ServiceContext.User.Identity.Name;
                // if status is Open, AssignedToID should be null
                if (issue.StatusID == IssueVisionServiceConstant.OpenStatusID)
                {
                    issue.AssignedToID = null;
                }
                // set ResolutionDate and ResolvedByID based on ResolutionID
                if (issue.ResolutionID == null || issue.ResolutionID == 0)
                {
                    issue.ResolutionDate = null;
                    issue.ResolvedByID = null;
                }
                else
                {
                    if (issue.ResolutionDate == null)
                        issue.ResolutionDate = DateTime.Now;
                    if (issue.ResolvedByID == null)
                        issue.ResolvedByID = ServiceContext.User.Identity.Name;
                }

                ObjectContext.Issues.AttachAsModified(issue, ChangeSet.GetOriginal(issue));
            }
            else
                throw new ValidationException(ErrorResources.NoPermissionToUpdateIssue);
        }

        public void DeleteIssue(Issue issue)
        {
            if ((issue.EntityState == EntityState.Detached))
            {
                ObjectContext.Issues.Attach(issue);
            }
            ObjectContext.Issues.DeleteObject(issue);
        }

        private class ServerBugCount
        {
            public Int32 Month { get; set; }
            public Int32 Year { get; set; }
            public Int32 Count { get; set; }
        }

        /// <summary>
        /// Return the active bug count for the last numberOfMonth months
        /// </summary>
        /// <param name="numberOfMonth"></param>
        /// <returns></returns>
        [Invoke]
        public string[] GetActiveBugCountByMonth(Int32 numberOfMonth)
        {
            var issueCountsByMonth = from n in ObjectContext.Issues
                                     where (n.ResolutionID == null || n.ResolutionID == 0)
                                     group n by new { month = n.OpenedDate.Month, year = n.OpenedDate.Year } into d
                                     select new ServerBugCount
                                     {
                                         Month = d.Key.month,
                                         Year = d.Key.year,
                                         Count = d.Count()
                                     };

            var resultList = new List<string>();
            // loop through issueCountsByMonth for the last numberOfMonth
            for (var i = numberOfMonth; i >= 1; i--)
            {
                var dt = DateTime.Today.AddMonths(-i);
                // search issueCountsByMonth for the issue counts of a specific month
                ServerBugCount currentIssueCount = issueCountsByMonth
                    .Where(n => (n.Month == dt.Month && n.Year == dt.Year))
                    .FirstOrDefault();

                if (currentIssueCount == null)
                {
                    // the active issue count for that month is zero
                    resultList.Add(dt.ToString("MM/yyyy") + "/0");
                }
                else
                {
                    // the active issue count for that month is not zero
                    resultList.Add(currentIssueCount.Month.ToString("00") + "/" + currentIssueCount.Year + "/" + currentIssueCount.Count);
                }
            }
            return resultList.ToArray();
        }

        /// <summary>
        /// Return the resolved bug count for the last numberOfMonth months
        /// </summary>
        /// <param name="numberOfMonth"></param>
        /// <returns></returns>
        [Invoke]
        public string[] GetResolvedBugCountByMonth(Int32 numberOfMonth)
        {
            var issueCountsByMonth = from n in ObjectContext.Issues
                                     where (n.ResolutionID != null && n.ResolutionID != 0)
                                     group n by new
                                     {
                                         month = (n.ResolutionDate ?? DateTime.Now).Month,
                                         year = (n.ResolutionDate ?? DateTime.Now).Year
                                     } into d
                                     select new ServerBugCount
                                     {
                                         Month = d.Key.month,
                                         Year = d.Key.year,
                                         Count = d.Count()
                                     };

            var resultList = new List<string>();
            // loop through issueCountsByMonth for the last numberOfMonth
            for (var i = numberOfMonth; i >= 1; i--)
            {
                var dt = DateTime.Today.AddMonths(-i);
                // search issueCountsByMonth for the issue counts of a specific month
                ServerBugCount currentIssueCount = issueCountsByMonth
                    .Where(n => (n.Month == dt.Month && n.Year == dt.Year))
                    .FirstOrDefault();

                if (currentIssueCount == null)
                {
                    // the resolved issue count for that month is zero
                    resultList.Add(dt.ToString("MM/yyyy") + "/0");
                }
                else
                {
                    // the resolved issue count for that month is not zero
                    resultList.Add(currentIssueCount.Month.ToString("00") + "/" + currentIssueCount.Year + "/" + currentIssueCount.Count);
                }
            }
            return resultList.ToArray();
        }

        private class ServerBugCountByPriority
        {
            public Int32 Priority { get; set; }
            public Int32 Count { get; set; }
        }

        /// <summary>
        /// Return the active bug count by priority
        /// </summary>
        /// <returns></returns>
        [Invoke]
        public string[] GetActiveBugCountByPriority()
        {
            var issueCountsByPriority = from n in ObjectContext.Issues
                                        where (n.ResolutionID == null || n.ResolutionID == 0)
                                        group n by new { priority = n.Priority } into d
                                        select new ServerBugCountByPriority
                                        {
                                            Priority = d.Key.priority,
                                            Count = d.Count()
                                        };

            var resultList = new List<string>();
            // loop through issueCountsByPriority
            for (var i = IssueVisionServiceConstant.HighestPriority; i <= IssueVisionServiceConstant.LowestPriority; i++)
            {
                // search issueCountsByPriority for the issue counts of a specific priority
                var priority = i;
                ServerBugCountByPriority currentIssueCount = issueCountsByPriority
                    .Where(n => (n.Priority == priority))
                    .FirstOrDefault();

                if (currentIssueCount == null)
                {
                    // the active issue count for that priority is zero
                    resultList.Add(i + "/0");
                }
                else
                {
                    // the active issue count for that priority is not zero
                    resultList.Add(currentIssueCount.Priority + "/" + currentIssueCount.Count);
                }
            }
            return resultList.ToArray();
        }

        /// <summary>
        /// Get a list of IssueHistory records for a specific IssueID
        /// </summary>
        /// <param name="issueID"></param>
        /// <returns></returns>
        public IQueryable<IssueHistory> GetIssueHistoryByIssueID(long issueID)
        {
            return ObjectContext.IssueHistories
                .Include("Platform")
                .Include("Status")
                .Where(n => n.IssueID == issueID)
                .OrderBy(n => n.DateCreated);
        }

        public IQueryable<IssueType> GetIssueTypes()
        {
            return ObjectContext.IssueTypes;
        }

        public IQueryable<Platform> GetPlatforms()
        {
            return ObjectContext.Platforms;
        }

        public IQueryable<Resolution> GetResolutions()
        {
            return ObjectContext.Resolutions;
        }

        public IQueryable<SecurityQuestion> GetSecurityQuestions()
        {
            return ObjectContext.SecurityQuestions;
        }

        public IQueryable<Status> GetStatuses()
        {
            return ObjectContext.Statuses;
        }

        public IQueryable<SubStatus> GetSubStatuses()
        {
            return ObjectContext.SubStatuses;
        }

        public IQueryable<User> GetUsers()
        {
            return ObjectContext.Users;
        }

        /// <summary>
        /// Get the user information for the currently login user
        /// </summary>
        /// <returns></returns>
        [Query(IsComposable = false)]
        public User GetCurrentUser()
        {
            return ObjectContext.Users.FirstOrDefault(u => u.Name == ServiceContext.User.Identity.Name);
        }

        public void InsertUser(User user)
        {
            // check for insert user permission
            if (CheckUserInsertPermission(user) && user.IsUserMaintenance)
            {
                // validate whether the user already exists
                User foundUser = ObjectContext.Users.Where(
                    n => n.Name == user.Name).FirstOrDefault();
                if (foundUser != null)
                    throw new ValidationException(ErrorResources.CannotInsertDuplicateUser);

                // Re-generate password hash and password salt
                user.PasswordSalt = HashHelper.CreateRandomSalt();
                user.PasswordHash = HashHelper.ComputeSaltedHash(user.NewPassword, user.PasswordSalt);

                // set a valid PasswordQuestion
                SecurityQuestion securityQuestion = ObjectContext.SecurityQuestions.FirstOrDefault();
                if (securityQuestion != null)
                    user.PasswordQuestion = securityQuestion.PasswordQuestion;
                // set PasswordAnswer that no body knows
                user.PasswordAnswerSalt = HashHelper.CreateRandomSalt();
                user.PasswordAnswerHash = HashHelper.CreateRandomSalt();

                // requires the user to reset profile
                user.ProfileReset = 1;

                if ((user.EntityState != EntityState.Detached))
                {
                    ObjectContext.ObjectStateManager.ChangeObjectState(user, EntityState.Added);
                }
                else
                {
                    ObjectContext.Users.AddObject(user);
                }
            }
            else
                throw new ValidationException(ErrorResources.NoPermissionToInsertUser);
        }

        public void UpdateUser(User currentUser)
        {
            if (currentUser.IsUserMaintenance)
            {
                // the call is from UserMaintenance screen
                if (currentUser.Name == ServiceContext.User.Identity.Name)
                {   // the caller is updateing itself
                    // Search user from database by name
                    User foundUser = ObjectContext.Users.FirstOrDefault(u => u.Name == currentUser.Name);
                    if (foundUser != null)
                    {
                        // verify whether the caller is admin
                        if (IsAdminUser(foundUser))
                        {
                            // Re-generate password hash and password salt
                            foundUser.PasswordSalt = HashHelper.CreateRandomSalt();
                            foundUser.PasswordHash = HashHelper.ComputeSaltedHash(currentUser.NewPassword, foundUser.PasswordSalt);

                            foundUser.FirstName = currentUser.FirstName;
                            foundUser.LastName = currentUser.LastName;
                            foundUser.Email = currentUser.Email;
                            foundUser.UserType = currentUser.UserType;
                            // requires the foundUser to reset profile
                            foundUser.ProfileReset = 1;
                        }
                        else
                            throw new ValidationException(ErrorResources.NoPermissionToUpdateUser);
                    }
                    else
                        throw new ValidationException(ErrorResources.NoPermissionToUpdateUser);
                }
                else
                {   // the caller is updating someone else
                    // verify whether the caller is admin
                    if (IsAdminUser(GetUserByName(ServiceContext.User.Identity.Name)))
                    {
                        // Re-generate password hash and password salt
                        currentUser.PasswordSalt = HashHelper.CreateRandomSalt();
                        currentUser.PasswordHash = HashHelper.ComputeSaltedHash(currentUser.NewPassword, currentUser.PasswordSalt);

                        // requires the currentUser to reset profile
                        currentUser.ProfileReset = 1;

                        ObjectContext.Users.AttachAsModified(currentUser, ChangeSet.GetOriginal(currentUser));
                    }
                    else
                        throw new ValidationException(ErrorResources.NoPermissionToUpdateUser);
                }
            }
            else
            {
                // the call is from MyProfile screen
                // and the caller can only update themselves
                if (currentUser.Name == ServiceContext.User.Identity.Name)
                {
                    // Search user from database by name
                    User foundUser = ObjectContext.Users.FirstOrDefault(u => u.Name == currentUser.Name);

                    if (foundUser != null)
                    {
                        // generate password hash
                        string passwordHash = HashHelper.ComputeSaltedHash(currentUser.Password, foundUser.PasswordSalt);

                        if (string.Equals(passwordHash, foundUser.PasswordHash, StringComparison.Ordinal))
                        {
                            // Re-generate password hash and password salt
                            foundUser.PasswordSalt = HashHelper.CreateRandomSalt();
                            foundUser.PasswordHash = HashHelper.ComputeSaltedHash(currentUser.NewPassword, foundUser.PasswordSalt);

                            // set the new password question
                            foundUser.PasswordQuestion = currentUser.PasswordQuestion;

                            // re-generate passwordAnswer hash and passwordAnswer salt
                            foundUser.PasswordAnswerSalt = HashHelper.CreateRandomSalt();
                            foundUser.PasswordAnswerHash = HashHelper.ComputeSaltedHash(currentUser.PasswordAnswer, foundUser.PasswordAnswerSalt);

                            foundUser.FirstName = currentUser.FirstName;
                            foundUser.LastName = currentUser.LastName;
                            foundUser.Email = currentUser.Email;
                            // no need to reset profile for the foundUser
                            foundUser.ProfileReset = 0;
                        }
                        else
                            throw new UnauthorizedAccessException(ErrorResources.PasswordDoesNotMatch);
                    }
                    else
                        throw new ValidationException(ErrorResources.NoPermissionToUpdateUser);
                }
                else
                    throw new ValidationException(ErrorResources.NoPermissionToUpdateUser);
            }
        }

        public void DeleteUser(User user)
        {
            // check for delete user permission
            if (CheckUserDeletePermission(user))
            {
                // validate the FOREIGN KEY contraint [FK_Issues_AssignedTo] from table [Issues]
                Issue foundIssue = ObjectContext.Issues.Where(
                    n => n.AssignedToID == user.Name).FirstOrDefault();
                if (foundIssue != null)
                    throw new ValidationException(ErrorResources.CannotDeleteUserAssignedToID);

                // validate the FOREIGN KEY contraint [FK_Issues_ChangedBy] from table [Issues]
                foundIssue = ObjectContext.Issues.Where(
                    n => n.ChangedByID == user.Name).FirstOrDefault();
                if (foundIssue != null)
                    throw new ValidationException(ErrorResources.CannotDeleteUserChangedByID);

                // validate the FOREIGN KEY contraint [FK_Issues_OpenedBy] from table [Issues]
                foundIssue = ObjectContext.Issues.Where(
                    n => n.OpenedByID == user.Name).FirstOrDefault();
                if (foundIssue != null)
                    throw new ValidationException(ErrorResources.CannotDeleteUserOpenedByID);

                // validate the FOREIGN KEY contraint [FK_Issues_ResolvedBy] from table [Issues]
                foundIssue = ObjectContext.Issues.Where(
                    n => n.ResolvedByID == user.Name).FirstOrDefault();
                if (foundIssue != null)
                    throw new ValidationException(ErrorResources.CannotDeleteUserResolvedByID);

                if ((user.EntityState == EntityState.Detached))
                {
                    ObjectContext.Users.Attach(user);
                }
                ObjectContext.Users.DeleteObject(user);
            }
            else
                throw new ValidationException(ErrorResources.NoPermissionToDeleteUser);
        }

        #region "Private Methods"

        private User GetUserByName(string userName)
        {
            return ObjectContext.Users.FirstOrDefault(u => u.Name == userName);
        }

        private static bool IsAdminUser(User user)
        {
            return (user.UserType == "A");
        }

        private bool IssueIsReadOnly(Issue currentIssue)
        {
            // Admin user can read/update any issue
            if (IsAdminUser(GetUserByName(ServiceContext.User.Identity.Name)))
                return false;

            // normal user can only read/update issues assigned to them
            // or issues created by them and have not assigned to anyone.
            if (currentIssue.AssignedToID != null
                && currentIssue.AssignedToID == ServiceContext.User.Identity.Name)
                return false;
            if (currentIssue.AssignedToID == null
                && currentIssue.OpenedByID == ServiceContext.User.Identity.Name)
                return false;

            return true;
        }

        private bool CheckUserInsertPermission(User user)
        {
            // cannot add itself
            if (user.Name == ServiceContext.User.Identity.Name)
                return false;

            // only admin user can insert a new user
            if (IsAdminUser(GetUserByName(ServiceContext.User.Identity.Name)))
                return true;
            return false;
        }

        private bool CheckUserDeletePermission(User user)
        {
            // cannot delete itself
            if (user.Name == ServiceContext.User.Identity.Name)
                return false;

            // only admin user can delete a user
            if (IsAdminUser(GetUserByName(ServiceContext.User.Identity.Name)))
                return true;
            return false;
        }

        #endregion "Private Methods"
    }
}